/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
class Cocos {
    public static long timeInUs() {
        return System.nanoTime();
    }

    public static int n = 0;
    public static class Vec3 {
        public static Vec3 set(Vec3 out, double x, double y, double z) {
            //System.out.println("This is set");
            out.x = x;
            out.y = y;
            out.z = z;
            return out;
        }
        public static Vec3 add(Vec3 out, Vec3 a, Vec3 b) {
            //System.out.println("This is add");
            out.x = a.x + b.x;
            out.y = a.y + b.y;
            out.z = a.z + b.z;
            return out;
        }
        public static Vec3 multiplyScalar(Vec3 out, Vec3 a, double b) {
            //System.out.println("This is multiplyScalar");
            out.x = a.x * b;
            out.y = a.y * b;
            out.z = a.z * b;
            return out;
        }
        public static Vec3 scaleAndAdd(Vec3 out, Vec3 a, Vec3 b, double scale) {
            //System.out.println("This is scaleAndAdd");
            out.x = a.x + b.x * scale;
            out.y = a.y + b.y * scale;
            out.z = a.z + b.z * scale;
            return out;
        }
        public static Vec3 copy(Vec3 out, Vec3 a) {
            //System.out.println("This is copy");
            out.x = a.x;
            out.y = a.y;
            out.z = a.z;
            return out;
        }
        public double x;
        public double y;
        public double z;
        Vec3(double x, double y, double z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }
    }
    public static class Particle {
        public ParticleSystemRenderCPU particleSystem;
        public Vec3 position;
        public Vec3 velocity;
        public Vec3 animatedVelocity;
        public Vec3 ultimateVelocity;
        public Vec3 startSize;
        public Vec3 size;
        public double randomSeed;
        public double remainingLifetime;
        public int loopCount;
        public int lastLoop;
        public double trailDelay;
        public double startLifetime;
        public double emitAccumulator0;
        public double emitAccumulator1;
        public double frameIndex;
        public int startRow;
        Particle(ParticleSystemRenderCPU particleSystem) {
            this.particleSystem = particleSystem;
            this.position = new Vec3(0, 0, 0);
            this.velocity = new Vec3(0, 0, 0);
            this.animatedVelocity = new Vec3(0, 0, 0);
            this.ultimateVelocity = new Vec3(0, 0, 0);
            this.startSize = new Vec3(0, 0, 0);
            this.size = new Vec3(0, 0, 0);
            this.randomSeed = 0; // uint
            this.remainingLifetime = 0.2; 
            this.loopCount = 0;
            this.lastLoop = 0;
            this.trailDelay = 0;
            this.startLifetime = 1;
            this.emitAccumulator0 = 0.0;
            this.emitAccumulator1 = 0.0;
            this.frameIndex = 0.0;
            this.startRow = 0;
        }
    }

    class Mode {
        public static final int Constant = 0;
        public static final int Curve = 1;
        public int TwoCurves = 2;
        public int TwoConstants = 3;
    }

    public static double repeat(double t, double length) {
        //System.out.println("This is repeat");
        return t - Math.floor(t / length) * length;
    }
    public static double wrapRepeat(double time, double prevTime, double nextTime) {
        //System.out.println("This is wrapRepeat");
        return prevTime + repeat(time - prevTime, nextTime - prevTime);
    }
    public static int binarySearchEpsilon(double[] array, double value, double epsilon) {
        //System.out.println("This is binarySearchEpsilon");
        int low = 0;
        int high = array.length - 1;
        int middle = high >>> 1;
        for (; low <= high; middle = (low + high) >>> 1) {
             n++;
            double test = array[middle];
            if (test > (value + epsilon)) {
                high = middle - 1;
            } else if (test < (value - epsilon)) {
                low = middle + 1;
            } else {
                return middle;
            }
        }
        return ~low;
    }
    public static double bezierInterpolate(double p0, double p1, double p2, double p3, double t) {
        //System.out.println("This is bezierInterpolate");
        n++;
        double u = 1 - t;
        double coeff0 = u * u * u;
        double coeff1 = 3 * u * u * t;
        double coeff2 = 3 * u * t * t;
        double coeff3 = t * t * t;
  		return coeff0 * p0 + coeff1 * p1 + coeff2 * p2 + coeff3 * p3;
    }
    public static class RealKeyframeValue {
        public double value = 0.0;
        public double rightTangent = 0.0;
        public double rightTangentWeight = 0.0;
        public double leftTangent = 0.0;
        public double leftTangentWeight = 0.0;
        RealKeyframeValue(double a, double b, double c, double d, double e) {
            this.value = a;
            this.rightTangent = b;
            this.rightTangentWeight = c;
            this.leftTangent = d;
            this.leftTangentWeight = e;
        }
    }
    public static double evalBetweenTwoKeyFrames(
            double prevTime,
            RealKeyframeValue prevValue,
            double nextTime,
            RealKeyframeValue nextValue,
            double ratio
    ) {
        //System.out.println("This is evalBetweenTwoKeyFrames");
        double dt = nextTime - prevTime;
        double ONE_THIRD = 1.0 / 3.0;
        boolean prevTangentWeightEnabled = false;
        boolean nextTangentWeightEnabled = false;
        double prevTangent = prevValue.rightTangent;
        double prevTangentWeightSpecified = prevValue.rightTangentWeight;
        double nextTangent = nextValue.leftTangent;
        double nextTangentWeightSpecified = nextValue.leftTangentWeight;

        if (!prevTangentWeightEnabled && !nextTangentWeightEnabled) {
            double p1 = prevValue.value + ONE_THIRD * prevTangent * dt;
            double p2 = nextValue.value - ONE_THIRD * nextTangent * dt;
            return bezierInterpolate(prevValue.value, p1, p2, nextValue.value, ratio);
        }
        return 0;
    }
    void assertIsTrue(boolean expr, String message) {
        if (!expr) {
            throw new Error("Assertion failed:");
        }
    }
    static class RealCurve {
        public static double value = 0.0;
        public static double rightTangent = 0.0;
        public static double rightTangentWeight = 0.0;
        public static double leftTangent = 0.0;
        public static double leftTangentWeight = 0.0;
        protected double[] _times = {0.1111111, 0.555555555, 0.999999999};
        protected RealKeyframeValue[] _values = {
                new RealKeyframeValue(0.2, 0.7, 0, 0.3, 0),
                new RealKeyframeValue(0.3, 0.4, 0, 0.2, 0),
                new RealKeyframeValue(0.4, 0.5, 0, 0.6, 0)
        };
        public double evaluate(double time) {
            //System.out.println("This is RealCurve evaluate");
            double[] times = this._times;
            RealKeyframeValue[] values = this._values;
            int nFrames = times.length;
            double firstTime = times[0];
            double lastTime = times[nFrames - 1];
            if (time < firstTime) {
                RealKeyframeValue preValue = values[0];
                time = wrapRepeat(time, firstTime, lastTime);
            }
            int index = binarySearchEpsilon(times, time, 1e-6);
            if (index >= 0) {
                return values[index].value;
            }
            int iNext = ~index;

            int iPre = iNext - 1;
			double preTime = times[iPre];
			RealKeyframeValue preValue = values[iPre];
            double nextTime = times[iNext];  
            RealKeyframeValue nextValue = values[iNext];
            
            double dt = nextTime - preTime;
            double ratio = (time - preTime) / dt;
            return evalBetweenTwoKeyFrames(preTime, preValue, nextTime, nextValue, ratio);
        }
    }
    public static class CurveRange {
        public int mode = Mode.Constant;
        public RealCurve spline = new RealCurve();
        public double constant = 1;
        public double multiplier = 1;
        CurveRange(int thismode) {
            this.mode = thismode;
        }
        public double evaluate(double time) {
            //System.out.println("This is CurveRange evaluate");
            switch (this.mode) {
                case Mode.Constant:
                    return this.constant;
                case Mode.Curve:
                    return this.spline.evaluate(time) * this.multiplier;
                default: return 0.0;
            }
        }
    }
    public static class SizeModule {
        public CurveRange size = new CurveRange(Mode.Curve);
        public CurveRange x = new CurveRange(Mode.Curve);
        public CurveRange y = new CurveRange(Mode.Curve);
        public CurveRange z = new CurveRange(Mode.Curve);
        public void animate(Particle particle, double dt) {
            Vec3.multiplyScalar(particle.size, particle.startSize,
                    this.size.evaluate(1 - particle.remainingLifetime / particle.startLifetime));
        }
    }
    public static class VelocityModule {
        public CurveRange x = new CurveRange(Mode.Curve);
        public CurveRange y = new CurveRange(Mode.Curve);
        public CurveRange z = new CurveRange(Mode.Curve);
        public CurveRange speedModifier = new CurveRange(Mode.Constant);
        public int space = 0;
        private boolean needTransform;
        public Vec3 _temp_v3 = new Vec3(0, 0, 0);
        VelocityModule() {
            this.speedModifier.constant = 1;
            this.needTransform = false;
        }
        public void animate(Particle p, double dt) {
            //System.out.println("This is animate");
            double normalizedTime = 1 - p.remainingLifetime / p.startLifetime;
            Vec3 vel = Vec3.set(this._temp_v3,
                    this.x.evaluate(normalizedTime),
                    this.y.evaluate(normalizedTime),
                    this.z.evaluate(normalizedTime));
            if (this.needTransform) {
            }
            Vec3.add(p.animatedVelocity, p.animatedVelocity, vel);
            Vec3.add(p.ultimateVelocity, p.velocity, p.animatedVelocity);
            Vec3.multiplyScalar(p.ultimateVelocity, p.ultimateVelocity,
                    this.speedModifier.evaluate(1 - p.remainingLifetime / p.startLifetime));
        }
    }
    public static class ParticleSystemRenderCPU {
        private Particle[] _particles;
        private SizeModule _sizeModule;
        private VelocityModule _velocityModule;
        ParticleSystemRenderCPU() {
            int size = 50;
            _particles = new Particle[size];
            for (int i = 0; i < size; i++) {
                _particles[i] = new Particle(this);
            }
            _sizeModule = new SizeModule();
            _velocityModule = new VelocityModule();
        }
        public void UpdateParticles(double dt) {
            //System.out.println("This is UpdateParticles");
            for (int i = 0; i < _particles.length; ++i) {
                Particle p = this._particles[i];
                Vec3.set(p.animatedVelocity, 0, 0, 0);
                Vec3.copy(p.ultimateVelocity, p.velocity);
                this._sizeModule.animate(p, dt);
                this._velocityModule.animate(p, dt);
                Vec3.scaleAndAdd(p.position, p.position, p.ultimateVelocity, dt); // apply velocity.
            }
        }
    }


}
/*
 * @State
 * @Tags Jetstream2
 */
class Benchmark {
    public static void main(String[] args) {
        runCocos();
    }
   /*
    * @Benchmark
    */
   static double runCocos(){
       Cocos.ParticleSystemRenderCPU[] systems = new Cocos.ParticleSystemRenderCPU[18];
       for (int i = 0; i < 18; i++) {
           systems[i] = new Cocos.ParticleSystemRenderCPU();
       }
       long start = Cocos.timeInUs();
       for (int j = 0; j < 600; j++) {
           for (int i = 0; i < 18; i++) {
               systems[i].UpdateParticles(0.5);
           }
       }
       long end = Cocos.timeInUs();
       long time = (end - start) ;
       System.out.println("res: " + systems);
       System.out.println("Cocos - RunCocos:"+ time / 1_000_000.0 +"ms");
       return time;
   }
}